Rust 生态蜜蜂|Unsafe 随堂小测题解 Part 7(完结)
本篇是对第三篇随堂小测[1]中第14题和第15题的题解。
题目与题解
第 14 题
以下是类型 IntoIter 的公开接口实现,其中存在什么问题?(10分) 请针对该类型的各公开接口构造测试,使以下代码触发 UB。(10分)
// 该 ManuallyDrop 类型被用于禁止编译器自动调用其包装类型 T 的析构函数
use std::mem::ManuallyDrop;
use std::{ptr, slice};
#[derive(Clone)]
pub struct IntoIter<T> {
// 构造一个堆上存储的切片类型
boxed_slice: Box<[ManuallyDrop<T>]>,
cur: usize,
}
impl<T> IntoIter<T> {
pub fn new(boxed_slice: Box<[T]>) -> Self {
let boxed_slice: Box<[ManuallyDrop<T>]> = unsafe {
let mut b = ManuallyDrop::new(boxed_slice);
let data: *mut ManuallyDrop<T> = b.as_mut_ptr().cast();
let len: usize = b.len();
// 通过data 和 len 构造切片
Box::from_raw(slice::from_raw_parts_mut(data, len))
};
Self {
boxed_slice,
cur: 0,
}
}
}
// 实现迭代器
impl<T> Iterator for IntoIter<T> {
type Item = T;
fn next(&mut self) -> Option<Self::Item> {
unsafe {
let item: ManuallyDrop<T> = ptr::read(self.boxed_slice.get(self.cur)?);
self.cur += 1; // 迭代游标指向下一个 Item
Some(ManuallyDrop::into_inner(item))
}
}
}
impl<T> Drop for IntoIter<T> {
fn drop(&mut self) {
unsafe {
let data: *mut T = self.boxed_slice.as_mut_ptr().add(self.cur).cast();
let len: usize = self.boxed_slice.len() - self.cur;
// 通过计算 data 和 len得到 slice
let slice: *mut [T] = slice::from_raw_parts_mut(data, len);
// 原地释放slice 内存
ptr::drop_in_place(slice);
}
}
}
上面代码是创建了一个堆上的切片,然后为其实现迭代器和Drop,其中用到了 `ManuallyDrop` 类型[2] 。该类型可以屏蔽编译器自动调用其包装类型 T
的析构函数。但是如果你调用了它提供了ManuallyDrop::into_inner
函数则会让 T
继续被自动析构。
基于这个思路,我们来构造 UB 代码:
fn main(){
let box_slice = Box::new([vec![1], vec![2], vec![3]]);
let mut iter = IntoIter::new(box_slice);
iter.next(); // 会导致 box_slice 第一个 Vec<i32> 被自动析构
// Undefined Behavior: pointer to alloc1711 was dereferenced after this allocation got freed
let iter_clone = iter.clone();
for i in iter_clone {
println!("{i:?}");
}
}
这个示例用 Miri
执行的时候会抛出 // error: Undefined Behavior: pointer to alloc1711 was dereferenced after this allocation got freed
这样的错误。
其实原因就在于:main函数中创建的 iter
在执行 next()
方法之后,其内部的第一个 Vec<i32>
被自动析构了。然而下一行 iter.clone()
又将 iter
深度拷贝了一次。
iter 的内存布局大概是这样:
// stack: []
// heap: ()
// layout:
// [ptr] -> (box slice ptr) -> (ManuallyDrop[Vec], ManuallyDrop[Vec], ManuallyDrop[Vec])
clone()
方法会被一层一层地调用,第一个元素的 Vec
内存已经被释放了,再clone
它的时候就会发生解引用已释放内存的问题,这是一个未定义行为,所以在代码执行的时候还不容易出现错误,但是靠 Miri 可以检测出来。
在 《Rust 编码规范》中有一个迭代器的规则 G.TRA.BLN.02 不要为迭代器实现Copy 特质[3] 。原因就是一般会存在有改变状态的迭代器,如果实现 Copy
,则可能会被意外隐式复制,违反 Rust 编译器可变借用独占原则,可能会导致一些意外行为。
所以,通常可以只为迭代器实现 Clone
,需要复制的时候显式拷贝。就像本例中的问题,clone
是显式的,如果出现问题,也容易排查。